/* eslint-disable @typescript-eslint/no-this-alias */
/*
 * Copyright The OpenTelemetry Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { type Span, context, diag, isSpanContextValid, trace } from '@opentelemetry/api';
import { InstrumentationBase, type InstrumentationConfig, InstrumentationNodeModuleDefinition, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';
// import { VERSION } from './version';

// -----------------------------------------------------------------------------
//  README
//  This file copies the pino instrumentation code so we can add the trace
//  field as documentated at
//  https://cloud.google.com/trace/docs/trace-log-integration#associating
//  We could potentially do this in the Pino configuration object's log()
//  method however this avoids looking up the async context.
// -----------------------------------------------------------------------------

// -----------------------------------------------------------------------------
//  Custom code
// -----------------------------------------------------------------------------

const gcpProjectId = [process.env.GCLOUD_PROJECT, process.env.GCP_PROJECT, process.env.PROJECT].find((val) => val?.length);
if (!gcpProjectId && process.env.TRACE_AGENT_ENABLED?.toLowerCase() === 'true') {
	throw new Error('GCLOUD_PROJECT, GCP_PROJECT, or PROJECT environment variable is required for Pino OpenTelemetry instrumentation');
}
const tracePrefix = `projects/${gcpProjectId}/traces/`;

// -----------------------------------------------------------------------------
//  Autogenerated version.js file in @opentelemetry/instrumentation-pino module
// -----------------------------------------------------------------------------

// this is autogenerated file, see scripts/version-update.js
const VERSION = '0.35.0';

// ----------------------------------------------------------------------------------------------------------------------------------------
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-pino/src/types.ts
// ----------------------------------------------------------------------------------------------------------------------------------------

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LogHookFunction = (
	span: Span,
	record: Record<string, any>,
	level?: number, // Available in pino >= 7.9.0
) => void;

export interface PinoInstrumentationConfig extends InstrumentationConfig {
	logHook?: LogHookFunction;

	/** Configure the names of field injected into logs when there is span context available.  */
	logKeys?: {
		traceId: string;
		spanId: string;
		traceFlags: string;
	};
}

// ----------------------------------------------------------------------------------------------------------------------------------------
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-pino/src/instrumentation.ts
// ----------------------------------------------------------------------------------------------------------------------------------------

const pinoVersions = ['>=5.14.0 <9'];

const DEFAULT_LOG_KEYS = {
	traceId: 'trace_id',
	spanId: 'span_id',
	traceFlags: 'trace_flags',
};

export class PinoInstrumentation extends InstrumentationBase {
	constructor(config: PinoInstrumentationConfig = {}) {
		super('@opentelemetry/instrumentation-pino', VERSION, config);
	}

	protected init() {
		return [
			new InstrumentationNodeModuleDefinition('pino', pinoVersions, (module, moduleVersion?: string) => {
				diag.debug(`Applying patch for pino@${moduleVersion}`);
				const isEsm = module[Symbol.toStringTag] === 'Module';
				const moduleExports = isEsm ? module.default : module;
				const instrumentation = this;
				const patchedPino = Object.assign((...args: unknown[]) => {
					if (args.length === 0) {
						return moduleExports({
							mixin: instrumentation._getMixinFunction(),
						});
					}

					if (args.length === 1) {
						// eslint-disable-next-line @typescript-eslint/no-explicit-any
						const optsOrStream = args[0] as any;
						if (typeof optsOrStream === 'string' || typeof optsOrStream?.write === 'function') {
							args.splice(0, 0, {
								mixin: instrumentation._getMixinFunction(),
							});
							return moduleExports(...args);
						}
					}

					args[0] = instrumentation._combineOptions(args[0]);

					return moduleExports(...args);
				}, moduleExports);

				if (typeof patchedPino.pino === 'function') {
					patchedPino.pino = patchedPino;
				}
				if (typeof patchedPino.default === 'function') {
					patchedPino.default = patchedPino;
				}
				if (isEsm) {
					if (module.pino) {
						// This was added in pino@6.8.0 (https://github.com/pinojs/pino/pull/936).
						module.pino = patchedPino;
					}
					module.default = patchedPino;
				}

				return patchedPino;
			}),
		];
	}

	override getConfig(): PinoInstrumentationConfig {
		return this._config;
	}

	override setConfig(config: PinoInstrumentationConfig) {
		this._config = config;
	}

	private _callHook(span: Span, record: Record<string, string>, level: number) {
		const hook = this.getConfig().logHook;

		if (!hook) {
			return;
		}

		safeExecuteInTheMiddle(
			() => hook(span, record, level),
			(err) => {
				if (err) {
					diag.error('pino instrumentation: error calling logHook', err);
				}
			},
			true,
		);
	}

	private _getMixinFunction() {
		const instrumentation = this;
		return function otelMixin(_context: object, level: number) {
			if (!instrumentation.isEnabled()) {
				return {};
			}

			const span = trace.getSpan(context.active());

			if (!span) {
				return {};
			}

			const spanContext = span.spanContext();

			if (!isSpanContextValid(spanContext)) {
				return {};
			}

			const logKeys = instrumentation.getConfig().logKeys ?? DEFAULT_LOG_KEYS;

			const record = {
				// Custom code --------------------------------------------------------------
				trace: tracePrefix + spanContext.traceId,
				// --------------------------------------------------------------------------
				[logKeys.traceId]: spanContext.traceId,
				[logKeys.spanId]: spanContext.spanId,
				[logKeys.traceFlags]: `0${spanContext.traceFlags.toString(16)}`,
			};

			instrumentation._callHook(span, record, level);

			return record;
		};
	}

	private _combineOptions(options?: any) {
		if (options === undefined) {
			return { mixin: this._getMixinFunction() };
		}

		if (options.mixin === undefined) {
			options.mixin = this._getMixinFunction();
			return options;
		}

		const originalMixin = options.mixin;
		const otelMixin = this._getMixinFunction();

		options.mixin = (context: object, level: number) => {
			return Object.assign(otelMixin(context, level), originalMixin(context, level));
		};

		return options;
	}
}
